半小时入门Spring

作为业界最知名的Java框架,Spring几乎是Java developer必须修炼的外功,全面地了解Spring是很有必要的。

What

首先,需要理清楚Spring的各种概念,避免混淆。

Spring Framework

Spring Framework是一个大引擎,也是Spring项目的核心部分,主要由20+个组件/模块构成,分为几大类:

  • 核心容器
    核心部分,提供最重要的IoC/DI功能,包含Core、Beans、Context等组件。
  • 数据处理与集成
    提供与数据库交互的抽象,如对JDBC、ORM等的封装。
  • Web
    提供基于Servlet的Web服务支撑。
  • AOP
    提供面向切面编程的支持。
  • 测试
    提供与JUnit等测试组件对接的支持。

Spring MVC

Spring MVC是Spring Framework的Web类模块中的一个,是一个提供Web服务的基础框架。

Spring Boot

Spring Boot是一个基于Spring Framework的解决方案套件或一个快捷方式。它集成了Web服务器、实现了Spring Framework的自动化配置以及解决了许多常用包的依赖问题,帮developer做了很多准备工作,因此developer可以很快地上手写业务代码而不必过于关注Spring的部分。

Spring Cloud

Spring Cloud基于Spring Boot,目的是解决更上层的问题,提供了一套完善的微服务框架,例如:

  • 分布式配置
    多个Spring Boot服务使用分布式配置服务进行配置。
  • 服务注册/发现/路由/调用
    多个Spring Boot服务间能够很方便地互相发现和调用。
  • 客户端负载均衡
    由Spring Boot服务进行负载均衡计算来决策调用其他服务的哪个实例。

这其中涉及到上层的内容在此不作进一步展开,详细的内容可参考《微服务二三事》

IoC

理清了各种五花八门的名词/框架/概念,让我们回归核心:IoC。
IoC(Inversion of Control)控制反转,光是这个词就能直接带出3个问题:

  1. 什么的控制?
  2. 为何反转?
  3. 如何反转?

让我们依次解答这几个问题。
在传统的Java代码里,我们面对那些Has-A的结构时是怎么做的呢?

1
2
3
4
5
6
7
8
9
public class B {
private A a;
public B(A a) {
this.a = a;
}
}
// somewhere else in code
A a = new A();
B b = new B(a);

这样的耦合度较强,代码中至少有一处需要:

  1. 显示或隐式地创建A对象。
  2. 传入B类的构造方法。

对于每一处Has-A都需要程序代码进行上述控制,当复杂度升高时难以掌控。
而IoC代替程序代码包办了这层控制,从而降低耦合度,程序代码只需向IoC控制器“要”依赖的对象,而不是自己“造”。
因此回答了头两个问题:反转的“控制”是对象间的依赖,使用一个统一的对象依赖控制器,目的是为了解耦。
可见,IoC具有以下特点:

  • 体现软件工程的原则之一:解耦。
  • 对实例的控制由程序转移到了控制器/容器/框架。
  • 为模块化提供了支撑。

实现

IoC只是一种设计,其实现并非Spring不可,例如可用策略模式+工厂模式来实现,当然也可以用依赖注入。

DI

DI(Dependency Injection)依赖注入是一种IoC的具体实现,被反转的控制是对象依赖的注值。以往程序代码主动为对象的依赖赋值,如今由DI的控制器统一处理。

Spring IoC & DI

Spring IoC定义了以下概念:

  • Bean
    等待被注入的依赖对象。
  • 容器
    上文提到的控制反转的控制器,用于管理所有Bean的依赖链。

在实际应用中,Spring DI提供三种方式注值:

  1. 构造方法注入。
  2. Setter方法注入。
  3. 成员变量注入。

Spring根据配置文件或注解进行Bean依赖的管理,主要完成了3项任务:

  1. 根据配置文件或注解,解析出Bean之间完整的依赖图。
  2. 利用Java反射,用适当的方式创建出Bean,保存到一个容器内。
  3. 再次利用反射,在适当的时机将被依赖的Bean作为成员变量注入到依赖其的Bean中。

AOP

AOP(Aspect-Oriented Programming)面向切面编程,是一种对OOP纵向结构丰富而横向结构缺失的补充。Java OOP可以提供复杂而纵深的继承体系,但是非继承关系的横向加强功能并不是那么容易实现。
例如,当很多类的方法都调用某一个类的成员方法时,需要这些类都持有该类的对象,且这些类的所有对象都需要显示地依赖该对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class A {
TheOne theOne;
public void setTheOne(TheOne theOne) {this.theOne = theOne;}
public void aDoSth() {
theOne.doSth();
... // business
}
}
public class B {
TheOne theOne;
public void setTheOne(TheOne theOne) {this.theOne = theOne;}
public void bDoSth() {
theOne.doSth();
... // business
}
}
public class TheOne{
public void doSth();
}

可以看到,A和B两个类中的部分代码几乎完全重复,而且与其业务不相关联,能否用某种方式实现同样的功能,而不需要developer写重复的代码逻辑呢?
一种方式是利用编译器将这些功能代码在编译阶段“织入”源代码,但这需要特殊的编译器。
另一种方式是利用动态代理在运行时将这些功能代码切入。

动态代理

Java的动态代理主要有JDK代理和CGLIB代理两种。

JDK代理

被代理的目标类需实现一个接口(业务相关的接口),没有在该接口定义的方法不能被代理,Spring AOP默认采用此方式进行代理,只有当被代理的类没有实现接口时才会切换到CGLIB代理。

CGLIB代理

这种方式需要CGLIB支持,相当于创建一个被代理的类的非继承代理子类(组合),因此也被称为子类代理。

概念

AOP引入了一系列概念,均和将功能代码切入有关。

通知Advice

定义了When和What,即切面的切入时机以及进行哪些行为。
可以想象,针对一个方法,合理的切入时机包括:

  1. 方法调用前。
  2. 方法执行后,不考虑返回。
  3. 方法返回后。
  4. 方法抛出异常后。
  5. 方法前以及方法后。
连接点JoinPoint

连接点可以理解为通知定义的上述切入时机。

切点PointCut

定义了Where,即匹配那些需要切入的方法,允许具体的方法名、类名、正则匹配等。

切面Aspect

在通知与切点的配合下,切面就成型了,程序就知道切入的When、Where、What。

引入Introducion

上述切面只是针对指定方法的增强,而引入可以针对类进行增强,为其动态地添加新的方法。

应用

最常见的两类应用就是权限控制和日志,此外还有缓存、调试、性能检测,甚至一定程度的事务处理。